/**
 * Copyright (c) 2012, David Varbel
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 * 
 * 1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * 2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.epistrata.experiment;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


/**
 * @author David
 *
 */
public class Experiment {
	private ArrayList<View> views = new ArrayList<View>();
	private ArrayList<Run> runs = new ArrayList<Run>();
	private Document config;
	private final int threads;
	
	private class RunHelper implements Runnable {

		private final Run run;
		private final ModelFactory factory;
		private CountDownLatch latch;
		
		RunHelper(Run run, ModelFactory factory, CountDownLatch latch) {
			this.run = run;
			this.factory = factory;
			this.latch = latch;
		}
		
		@Override
		public void run() {
			Experiment.this.sendRunStart(run);
			Model model = factory.newInstance(run);
			run.setStartTime();
			model.run();
			run.setFinishTime();
			Experiment.this.sendRunFinish(run);
			latch.countDown();
		}
		
	}
	
	/**
	 * 
	 * @param configFilename
	 * @param designMatrixFilename
	 * @param customViews
	 * @param threads
	 */
	public Experiment(String configFilename, String designMatrixFilename, List<View> customViews, int threads) {
		this.threads = threads;
		
		//create views
		views.add(new ConsoleView());
		views.addAll(customViews);
		
		for (View view : views) {
			try {
				view.start();
			} catch (Exception e) {
				StringBuffer stackTrace = new StringBuffer();
				for(StackTraceElement element : e.getStackTrace()) {
					stackTrace.append("\t" + element.toString() + "\n");
				}
				System.out.println("Failed to update view: " + e + "\n" + stackTrace);
			}
		}

		File configFile = new File(configFilename);
		if(!configFile.exists()) {
			throw new IllegalArgumentException("Experiment configuration file provided does not exist.");
		}
		
		try {
			DocumentBuilder builder;
			builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			config = builder.parse(configFile);
			config.getDocumentElement().normalize();
		}/* catch (SAXException|IOException|ParserConfigurationException e) {
			throw new IllegalArgumentException("Error processing config file.", e);
		}*/catch (Exception e) {
			throw new IllegalArgumentException("Error processing config file.", e);
		}
	
		//parse design matrix into runs
		parseRuns(designMatrixFilename);
		
	}
	
	public void runExperiment(ModelFactory factory) {
		
		ExecutorService pool = Executors.newFixedThreadPool(threads);
		//ExecutorService pool = Executors.newSingleThreadExecutor();
		CountDownLatch latch = new CountDownLatch(runs.size());
		
		//queue up runs
		for (Run run : runs) {
			pool.execute(new RunHelper(run, factory, latch));
		}
		
		//wait until all complete
		try {
			latch.await();
		} catch (InterruptedException e) {
			StringBuffer stackTrace = new StringBuffer();
			for(StackTraceElement element : e.getStackTrace()) {
				stackTrace.append("\t" + element.toString() + "\n");
			}
			System.out.println("Run thread interrupted: " + e + "\n" + stackTrace);
		}
		
		pool.shutdown();
				
		for (View view : views) {
			try {
				view.close();
			} catch (Exception e) {
				StringBuffer stackTrace = new StringBuffer();
				for(StackTraceElement element : e.getStackTrace()) {
					stackTrace.append("\t" + element.toString() + "\n");
				}
				System.out.println("Failed to updated view: " + e + "\n" + stackTrace);
			}
		}
		
	}

	private void parseRuns(String experimentDesignFilename) {
		//set up response types
		ArrayList<String> responseTypes = new ArrayList<String>();
		NodeList responses = config.getElementsByTagName("epi:response");
		for (int respNum = 0; respNum < responses.getLength(); respNum++) {
			Element response = (Element) responses.item(respNum);
			responseTypes.add(response.getAttribute("id"));
		}
		
		File inputFile = new File(experimentDesignFilename);
		BufferedReader in = null;
		
		if(!inputFile.exists()) {
			throw new IllegalArgumentException("Experiment design file provided does not exist.");
		}
		
		try {
			in = new BufferedReader(new FileReader(inputFile));
						
			String lineFromFile;
			
			//load line headers
			lineFromFile = in.readLine();
			String[] columnHeaders = lineFromFile.split("\t");
			
			//match line headers with factor properties from config document
			String[] high = new String[columnHeaders.length];
			String[] low = new String[columnHeaders.length];
			NodeList factorList = config.getElementsByTagName("epi:factor");
			for (int colNum = 0; colNum < columnHeaders.length; colNum++) {
				Element factorElement = null;
				for (int factorNum = 0; factorNum < factorList.getLength(); factorNum++) {
					Element temp = (Element) factorList.item(factorNum);
					if (temp.getAttribute("id").equals(columnHeaders[colNum])) {
						factorElement = temp;
						break;
					}
				}
				if (factorElement == null) {
					throw new IllegalArgumentException("Design file column header \"" + columnHeaders[colNum] + "\" does not match a factor defined in the config file.");
				}
				high[colNum] = factorElement.getElementsByTagName("epi:high").item(0).getFirstChild().getNodeValue();
				low[colNum] = factorElement.getElementsByTagName("epi:low").item(0).getFirstChild().getNodeValue();
			}

			
			//each line defines a run
			int runNumber = 0;
			NodeList seeds = config.getElementsByTagName("epi:randomSeed");
			while ((lineFromFile = in.readLine()) != null) {
				//build map of factors with values for this run
				HashMap<String, String> factors = new HashMap<String, String>();
				String[] levels = lineFromFile.split("\t");
				for (int colNum = 0; colNum < columnHeaders.length && colNum < levels.length; colNum++) {
					if (levels[colNum].equals("+")) {
						factors.put(columnHeaders[colNum], high[colNum]);
					} else if (levels[colNum].equals("-")) {
						factors.put(columnHeaders[colNum], low[colNum]);
					} else {
						throw new IllegalArgumentException("Invalid character in design file: \"" + levels[colNum] + "\".  Only \"+\" or \"-\" allowed");
					}
				}
				//replicate each run for each random seed
				int seedNum = 0;
				do { //if no replication specified, still create runs
					long seed;
					if (seeds.getLength() > 0) {
						try {
							seed = Long.parseLong(seeds.item(seedNum).getFirstChild().getNodeValue());
						} catch (NumberFormatException e) {
							message(null, "Random seed number " + Integer.toString(seedNum) + " provided in the config file cannot be converted to a long.  Using current time instead.");
							seed = System.currentTimeMillis();
						}
					} else {
						seed = System.currentTimeMillis();
					}
					runs.add(new Run(this, runNumber, seed, factors, responseTypes));
					seedNum++;
				} while(seedNum < seeds.getLength());
				runNumber++;
			}
			
		} 
		catch (IOException e) {
			throw new IllegalArgumentException("Error reading experiment design file.", e);
		} finally {
			try {
				in.close();
			} catch (IOException e) {
				//Do nothing.  We tried!
			}
			
		}
		
	}

	void message(Run run, String msg) {
		for (View view : views) {
			try {
			view.handleMessage(run, msg);
			} catch (Exception e) {
				StringBuffer stackTrace = new StringBuffer();
				for(StackTraceElement element : e.getStackTrace()) {
					stackTrace.append("\t" + element.toString() + "\n");
				}
				System.out.println("Failed to updated view: " + e + "\n" + stackTrace);
			}
		}
	}
	
	void sendRunStart(Run run) {
		for (View view : views) {
			try {
				view.handleRunStart(run);
			} catch (Exception e) {
				StringBuffer stackTrace = new StringBuffer();
				for(StackTraceElement element : e.getStackTrace()) {
					stackTrace.append("\t" + element.toString() + "\n");
				}
				System.out.println("Failed to updated view: " + e + "\n" + stackTrace);
			}
		}
	}

	void sendRunFinish(Run run) {
		for (View view : views) {
			try {
				view.handleRunFinish(run);
			} catch (Exception e) {
				StringBuffer stackTrace = new StringBuffer();
				for(StackTraceElement element : e.getStackTrace()) {
					stackTrace.append("\t" + element.toString() + "\n");
				}
				System.out.println("Failed to updated view: " + e + "\n" + stackTrace);
			}
		}
	}
	
	void sendDataUpdate(Run run, Model model) {
		for (View view : views) {
			try {
				view.handleDataUpdate(run, model);
			} catch (Exception e) {
				StringBuffer stackTrace = new StringBuffer();
				for(StackTraceElement element : e.getStackTrace()) {
					stackTrace.append("\t" + element.toString() + "\n");
				}
				System.out.println("Failed to updated view: " + e + "\n" + stackTrace);
			}
		}
	}
	
	ArrayList<Run> getRuns() {
		return runs;
	}
}
